/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: RadioServerContentDirectory.java,v $
 *
 */

package com.cidero.server;

import java.io.OutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.PrintWriter;
import java.io.IOException;
import java.io.FileNotFoundException;

import java.util.ArrayList;
import java.util.List;
import java.util.Hashtable;
import java.util.logging.Logger;

import org.cybergarage.upnp.Action;
import org.cybergarage.upnp.device.InvalidDescriptionException;

import com.cidero.upnp.*;
import com.cidero.util.MrUtil;
import com.cidero.util.URLUtil;
import com.cidero.util.NetUtil;
import com.cidero.util.ShoutcastSnooper;

/**
 * UPnP ContentDirectory implementation for simple metadata-only server.
 * Parses a metadata database directory tree containing a set of XML 
 * files formatted as DIDL-Lite, and exports the metadata via UPnP.
 * A proxy server is built-in to support some (limited) synchronization
 * capability for multiple renderers reading the same content. 
 *
 * Primary application for this is serving up of radio station data, but
 * there may be others.
 *
 * Database directory:
 *
 *   Location of database directory is specified using the 'databaseDir'
 *   property in the RadioServer.properties file
 *  
 *   Current convention for database directory tree is:
 *
 *   <rootDir>
 *    |
 *    |--AllStations     
 *    |   |--Station1
 *    |   |--Station2
 *    |   |--Station<N>
 *    |
 *    |--Favorites
 *    |   |
 *    |   |--User1
 *    |   |   |--Station1
 *    |   |   |--Station2
 *    |   |   |--Station<N>
 *    |   |
 *    |   |-User2
 *    |   |   |--Station1
 *    |   |   |--Station2
 *    |   |   |--Station<N>
 *    |
 *    |--Genres
 *    |   |
 *    |   !--Rock
 *    |   |   |--Station<1...N>
 *    |   !
 *    |   !--Classical
 *    |   |   |--Station<1...N>
 *    |   !
 *    |   !--Country
 *    |   |   |--Station<1...N>
 *
 *  One psuedo-requirement of this server is that an 'AllStations' directory
 *  exists, with a flat file structure containing a single metadata file
 *  for each station (no subdirectories). This convention allows for the
 *  automatic generation of other categories from the master list, such
 *  as the 'Genres' category, which is generated by the server automatically
 *  if there is no 'Genres' subdirectory in the database.
 *
 *  Users are free to create other directories/subdirectories as desired,
 *  and they will show up in the server browse tree.  These custom 
 *  directories should simply contain copies of the desired stations 
 *  from the AllStations directory.
 *
 *  Oh yeah, it's better if you don't put spaces in filenames yet!
 *
 *  TODO:  No watched folders implemented yet (or true support for 
 *  SystemUpdateID events). Users currently must stop/restart the server
 *  after adding stations.  Also need to support UPnP create object 
 *  actions so new stations can be added via control point GUI
 */
public class RadioServerContentDirectory  extends ContentDirectory
{
  private static Logger logger = Logger.getLogger("com.cidero.server");

  // Service's parent device
  RadioServer device;  

  // Root container for media tree
  CDSStorageFolder rootFolder; 

  CDSStorageFolder allStationsFolder;
  CDSStorageFolder genresFolder;


  // Hash table of objects, using UPnP object ID as key
  Hashtable objHashTable = new Hashtable();
  

  /**
   * Constructor
   *
   */
  public RadioServerContentDirectory( RadioServer device ) 
    throws InvalidDescriptionException
  {
    super( device );
    
    logger.fine("Entered RadioServerContentDirectory constructor");

    this.device = device;

    initializeStateVariables();
    
    initMediaTree();

    logger.fine("Leaving RadioServerContentDirectory constructor");
  }
	
  public void initializeStateVariables()
  {
    setStateVariable("SystemUpdateID", "1" );
  }

  /**
   * Initialize the media tree from the file-based database. 
   *
   */
  public void initMediaTree()
  {
    logger.fine("Initializing media tree");
    
    // Root object
    rootFolder = new CDSStorageFolder();
    rootFolder.setId("0");
    rootFolder.setParentId("-1");
    rootFolder.setTitle("Root");
    rootFolder.setCreator("UNKNOWN");
    objHashTable.put( rootFolder.getId(), rootFolder );

    // Load the actual content into the CDS heirarchy

    String dbDir = device.getDatabaseDir();

    logger.info("Loading all stations from db " + dbDir );
    File rootDir = new File( dbDir );

    // load folders (recursive descent)
    loadCDSFolder( rootFolder, rootDir );

    // Print warning if no AllStations folder created (non-fatal error)
    allStationsFolder =
      (CDSStorageFolder)rootFolder.searchByTitle("AllStations");
    if( allStationsFolder == null )
      logger.warning("No AllStations directory found in database!");

    //
    // If Genre folder didn't exist in database directory, create one
    // automatically from all the objects in the AllStations folder
    //
    genresFolder = (CDSStorageFolder)rootFolder.searchByTitle("Genres");
    if( genresFolder == null )
    {
      if( allStationsFolder != null )
      {
        logger.info("Generating Genres folder");
        genresFolder = new CDSStorageFolder();
        genresFolder.setId( rootFolder.getId() + "/Genres" );
        genresFolder.setCreator( rootFolder.getCreator() );
        genresFolder.setTitle("Genres");
        rootFolder.addChild( genresFolder );  // sets parentId
        // Set the unique Ids based on the parent rootFolder
        objHashTable.put( genresFolder.getId(), genresFolder );

        populateGenresFolder( genresFolder, allStationsFolder );
        genresFolder.sortByTitle();
      }
    }

    rootFolder.sortByTitle();

  }

  /**
   *  Load storage folder from directory containing XML DIDL-Lite files.
   *  This function descends recursively through the metadata directory tree.
   */
  public void loadCDSFolder( CDSStorageFolder folder, File dir )
  {
    logger.fine("Loading CDS Folder " + folder.getTitle() );

    int subDirCount = 0;
    int fileCount = 0;
      
    // Check for and process all subdirectories first.
    File[] fileList = dir.listFiles();
    for( int n = 0 ; n < fileList.length ; n++ )
    {
      if( fileList[n].isDirectory() )
      {
        logger.fine("Processing subdir " + fileList[n].getName() );

        // Create folder for subdirectory, give it a unique id in the
        // CDS heirarchy, and add to master object hash table
        CDSStorageFolder subDirFolder = new CDSStorageFolder();
        //subDirFolder.setId( folder.getId() + "/" + subDirCount );
        subDirFolder.setId( folder.getId() + "/" + 
                            fileList[n].getName().replaceAll(" ", "_") );
        subDirFolder.setTitle( fileList[n].getName().replaceAll(" ", "_") );
        objHashTable.put( subDirFolder.getId(), subDirFolder );

        // recursively descend into subdir
        loadCDSFolder( subDirFolder, fileList[n] );

        folder.addChild( subDirFolder );
        subDirCount++;
      }
      else 
      {
        fileCount++;
      }
    }

    if( fileCount == 0 )
    {
      logger.fine("No files for folder " + folder.getTitle() );
      return;
    }
      
    //
    // Load all station files in directory. First, load them all into 
    // a single CDSObjectList so they can be sorted by title 
    // (directory listing order can be random)
    //
    CDSObjectList stationsObjList = new CDSObjectList();
      
    for( int n = 0 ; n < fileList.length ; n++ )
    {
      String filename;

      try
      {
        filename = fileList[n].getCanonicalPath();
      }
      catch( IOException e )
      {
        logger.warning("IOException accessing file - skipping");
        continue;
      }
      
      if( ! filename.endsWith(".xml") )
      {
        logger.fine("Skipping non-XML file " + filename );
        continue;
      }
      else
      {
        logger.fine("Processing station file " + filename );
      }
        
      CDSObjectList fileObjList;

      try 
      {
        fileObjList = new CDSObjectList( new FileInputStream(filename ) );
      }
      catch( UPnPException e )
      {
        logger.warning("Exception while processing database file: " + 
                       filename + " - skipping\n" );
        continue;
      }
      catch( FileNotFoundException e )
      {
        logger.warning("File '" + filename + "' not found" );
        continue;
      }

      //
      // Add item object list to all stations category. DIDL-Lite files in
      // allStations directory have only one object (station) per file by
      // convention. Handle multiple station case, but print warning 
      // for now to let user know it is not recommended
      //
      if( fileObjList == null || fileObjList.size() == 0 )
      {
        logger.warning("Error processing station file " + filename +
                       " No stations found");
      }
      else
      {
        if( fileObjList.size() > 1 )
        {
          logger.warning("Found more than 1 station in station file " +
                         filename + " This is not recommended" );
        }
        
        for( int i = 0 ; i < fileObjList.size() ; i++ )
        {
          CDSObject stationObj = (CDSObject)fileObjList.get(i);
          if( ! (stationObj instanceof CDSAudioBroadcast) )
          {
            logger.warning("Item is not of class audioBroadcastItem!");
            continue;
          }

          stationsObjList.add( stationObj );
        }
      }
    }
      
    stationsObjList.sortByTitle();
      
    // Now put the sorted object list in a CDSStorageFolder container
    for( int n = 0 ; n < stationsObjList.size() ; n++ )
    {
      CDSObject stationObj = (CDSObject)stationsObjList.get(n);

      // 
      // Convert the simple single node DIDL-Lite to a small folder
      // tree for this station for convenient browsing of different
      // streaming formats/bitrates supported by the station
      //
      CDSPlaylistContainer stationFolder =
        convertStationObj( folder, (CDSAudioBroadcast)stationObj, n );

      logger.fine("Addion stationFolder " + stationFolder.getTitle() + 
                  " to folder " + folder.getTitle() );

      // Add the station folder to the parent folder
      folder.addChild( stationFolder );
    }

    logger.fine("Done loading CDS Folder " + folder.getTitle() +
                " stationCount = " + folder.getChildCount() );
  }
  
  /** 
   * Convert the simple single node DIDL-Lite to a small folder
   * tree for this station for convenient browsing of different
   * streaming formats/bitrates supported by the station. 
   *
   * When exported, the final station folder structure looks something like:
   *
   *    <ParentFolder> (e.g.  'AllStations')
   *     |
   *     |-Groove Salad (folder)
   *     |   |
   *     |   |-Groove Salad - MPEG/64k (folder)       -> Playlist resource 
   *     |   |  |-Groove Salad - MPEG/64k #1 (item)   -> MPEG resource 
   *     |   |  |-Groove Salad - MPEG/64k #2 (item)   -> .
   *     |   |  |-Groove Salad - MPEG/64k #3 (item)   -> .
   *     |   |
   *     |   |-Groove Salad - MPEG/128k (folder)      -> Playlist resource 
   *     |   |  |-Groove Salad - MPEG/128k #1         -> MPEG resource 
   *     |   |  |-Groove Salad - MPEG/128k #2         -> .
   *     |   |  |-Groove Salad - MPEG/128k #3         -> .
   *
   * The individual resources in the case of a incoming playlist resource
   * aren't expanded until a browse is done on the parent folder (If 
   * station list is large, don't want to have to load all M3Us from the
   * net at startup)
   *
   * (BELOW NOT YET TRUE - PERHAPS A TO-DO)
   * The exported playlist objects are always of type extended-M3U
   * ( MIME-type 'x-mpegurl' ) for consistency. If stations on the web
   * publish playlist in PLS format, they are converted to M3U prior 
   * to export to MediaRenderer devices by this server. The reason for 
   * this is that the Intel NMPR guidelines recommend that all renderers
   * implement support for x-mpegurl, so it *should* be supported by 
   * the great majority of renderer devices in the future.
   */
  public CDSPlaylistContainer convertStationObj( CDSStorageFolder parentFolder,
                                                 CDSAudioBroadcast obj,
                                                 int fileNum )
  {
    //
    // Use playlist container objects for audio broadcast 'folders', since
    // they support most of the same fields (genre, description, etc...) 
    //
    CDSPlaylistContainer stationFolder = new CDSPlaylistContainer();

    logger.fine("Converting station obj: " + obj.getTitle() );

    stationFolder.setId( parentFolder.getId() + "/" + fileNum );
    stationFolder.setCreator( obj.getCreator() );
    stationFolder.setTitle( obj.getTitle() );
    stationFolder.setGenre( obj.getGenre() );
    stationFolder.setDescription( obj.getDescription() );
    
    // Add object reference to folder so the AudioItemBroadcast 
    // metadata can be recovered from the folder later
    stationFolder.setExtraMetadataObj( obj );

    objHashTable.put( stationFolder.getId(), stationFolder );

    for( int n = 0 ; n < obj.getResourceCount(); n++ )
    {
      CDSPlaylistContainer formatFolder = new CDSPlaylistContainer();

      CDSResource resource = obj.getResource(n);
      
      logger.fine("Converting station obj resource: " +  resource.getName() );

      String protocolInfo = resource.getProtocolInfo();
      int bitRateBytesPerSec = resource.getBitRate();
      int bitRateKilobitsPerSec = (bitRateBytesPerSec*8)/1024;
      
      formatFolder.setId( stationFolder.getId() + "/Format_" + (n+1) );
      formatFolder.setTitle( obj.getTitle() +
                             " - MPEG/" + bitRateKilobitsPerSec + "k" );
      formatFolder.setCreator( obj.getCreator() );
      formatFolder.setGenre( obj.getGenre() );
      formatFolder.setDescription( obj.getDescription() );
      formatFolder.addResource( resource );
      
      // Add object reference to folder so the AudioItemBroadcast 
      // metadata can be recovered from the folder later
      formatFolder.setExtraMetadataObj( obj );

      objHashTable.put( formatFolder.getId(), formatFolder );

      if( resource.isPlaylist() )  // M3U, PLS
      {
        // Playlist resource. Leave the formatFolder empty. It is filled
        // on-demand at browse time by loading playlist from the internet
        // station. Event though it is empty, set the childCount to 99 
        // so control points won't skip over the Browse. When the playlist
        // is read from the internet, the childCount is updated to the 'true'
        // count (presumably < 99)
        formatFolder.setChildCount(99);
      }
      else
      {
        // Single link per format/bitrate case. Add single child to 
        // sub-folder.
        
        CDSAudioBroadcast objClone = (CDSAudioBroadcast)obj.clone();
        objClone.setId( formatFolder.getId() + "/stream_1" );
        objClone.setCreator( obj.getCreator() );
        objClone.setTitle( formatFolder.getTitle() + " #1" );
        formatFolder.addChild( objClone );
      }
      
      stationFolder.addChild( formatFolder );
    }

    return stationFolder;
  }
  
  /**
   * Populate 'Genres' folder automatically from 'AllStations' folder 
   * making use of the <upnp:genre> property in the objects in that folder.
   *
   * The genre field for each radio station may multiple genres, separated
   * by commas, and each genre may have  a 'sub-genre'. A complex radio
   * station genre example would look like the following in the XML file:
   *
   *   <genre>Rock/Metal,Alternative</genre>
   *
   * This routine will create folders for the genres 'Rock' and 'Alternative'
   * if necessary, and put copies of the radio station in question into both
   * of those folders. It will *not* create sub-folders for the sub-genres
   * at the moment, since there just aren't enough stations to justify
   * splitting them up into sub-folders (yet)
   * 
   * @param  genresFolder
   * @param  allStationsFolder
   */
  public CDSStorageFolder
  populateGenresFolder( CDSStorageFolder genresFolder, 
                        CDSStorageFolder allStationsFolder )
  {
    // 
    // At time of this method's invocation, all objects in allStationsFolder
    // should have been converted to playlist container objects
    //
    for( int n = 0 ; n < allStationsFolder.getChildCount() ; n++ )
    {
      CDSObject tmpObj = allStationsFolder.getChild(n);
      if( ! (tmpObj instanceof CDSPlaylistContainer) )
      {
        logger.fine("Skipping non-playlist container in AllStations Folder");
        continue;
      }
      
      CDSPlaylistContainer stationFolder = (CDSPlaylistContainer)tmpObj;

      // break up comma-separated genre list.
      String[] genreList = stationFolder.getGenre().split(",");

      for( int g = 0 ; g < genreList.length ; g++ )
      {
        String genre = genreList[g].trim();

        // Trim off sub-genre if present
        int index = genre.indexOf("/");
        if( index >= 0 )
          genre = genre.substring(0,index);

        if( genre == null )
          genre = "Other";

        logger.fine("Copying station " + tmpObj.getTitle() +
                    " to Genres folder " + genre );

        //
        // Check if genre sub-folder exists. If so, copy station folder to
        // it. Otherwise create a new sub-folder  
        //
        CDSStorageFolder genreSubFolder =
        (CDSStorageFolder)genresFolder.searchByTitle( genre );
        if( genreSubFolder == null )
        {
          genreSubFolder = new CDSStorageFolder();        
          genreSubFolder.setTitle( genre );

          genreSubFolder.setId( genresFolder.getId() + "/" + 
                                genresFolder.getChildCount() );
          genresFolder.addChild( genreSubFolder );

          objHashTable.put( genreSubFolder.getId(), genreSubFolder );
        }

        //
        // Need to clone station object since it needs to be assigned new
        // unique ids when placed under Genre folder heirarchy
        // Note that the clone() method for containers will properly clone
        // all child objects in the container (deep copy). However, assigning
        // new unique ids is not done in clone routine, since the id generation
        // scheme may differ for different apps.
        //

        CDSPlaylistContainer stationFolderClone =
          (CDSPlaylistContainer)stationFolder.clone();
      
        stationFolderClone.setId( genreSubFolder.getId() + "/" + 
                                  genreSubFolder.getChildCount() );

        genreSubFolder.addChild( stationFolderClone );

        // Walk the folder tree, setting the child object ids using a
        // simple integer path scheme
        setIntegerPathChildIds( stationFolderClone );

        // Now that objects have ids, add them to hash table (another 
        // recursive tree walk)
        addContainerToHashTable( stationFolderClone );

      } // for( int g = 0 ; g < genreList.length ; g++ )
      
    } // for( int n = 0 ; n < allStationsFolder.getChildCount() ; n++ )

    return genresFolder;
  }
  
  /**
   *  Set unique ids for container. Use simple heirarchical scheme based on 
   *  parent id + an integer path, where the integer is simply the index
   *  of the child object within its parent container.
   *
   *  Example:
   * 
   *     <topLevelId>/1/3
   *
   */
  public void setIntegerPathChildIds( CDSContainer container )
  {
    for( int i = 0 ; i < container.getChildCount() ; i++ )
    {
      CDSObject childObj = container.getChild(i);
      if( childObj == null )
        continue;   // unexpanded playlist resource - skip
       
      childObj.setParentId( container.getId() );
      childObj.setId( container.getId() + "/" + i );

      if( childObj instanceof CDSContainer )
        setIntegerPathChildIds( (CDSContainer)childObj );  
    }
  }

  /**
   *  Add objects to hash table (recursive tree walk)
   *
   */
  public void addContainerToHashTable( CDSContainer container )
  {
    objHashTable.put( container.getId(), container );

    for( int i = 0 ; i < container.getChildCount() ; i++ )
    {
      CDSObject childObj = container.getChild(i);

      if( childObj == null )
        continue;   // unexpanded playlist resource - skip

      objHashTable.put( childObj.getId(), childObj );

      if( childObj instanceof CDSContainer )
        addContainerToHashTable( (CDSContainer)childObj );
    }
  }


  /**
   * Browse the CDS heirarchy
   *
   * @param  action  UPnP action object
   *
   */
  public boolean actionBrowse( Action action )
  {
    String objectID = action.getArgumentValue("ObjectID");
    String browseFlag = action.getArgumentValue("BrowseFlag");
    String filter = action.getArgumentValue("Filter");
    String sortCriteria = action.getArgumentValue("SortCriteria");
    if( sortCriteria == null )
      sortCriteria = "";
    int startingIndex = action.getArgumentIntegerValue( "StartingIndex" );
    int requestedCount = action.getArgumentIntegerValue( "RequestedCount" );
            
    String threadName = Thread.currentThread().getName();
    
    logger.fine("[ " + threadName + "] " + " objectID = " + objectID );
    logger.fine("browseFlag = " + browseFlag );
    logger.fine("filter = " + filter );
    logger.fine("sortCrit = " + sortCriteria );

    String result = "";
    int[] numMatches = new int[1];
    int[] totalMatches = new int[1];

    if( browseFlag.equals("BrowseDirectChildren") )
    {
      // Not *quite* finished with support for multiple LAN interfaces TODO
      result = BrowseChildren( NetUtil.getDefaultLocalIPAddress(),
                               objectID, filter, startingIndex,
                               requestedCount, sortCriteria,
                               numMatches, totalMatches );

      action.setArgumentValue( "NumberReturned", numMatches[0] );
      action.setArgumentValue( "TotalMatches", totalMatches[0] );
    }
    else if( browseFlag.equals("BrowseMetadata") )
    {
      result = BrowseMetadata( NetUtil.getDefaultLocalIPAddress(),
                               objectID, filter, startingIndex,
                               requestedCount, sortCriteria );

      action.setArgumentValue( "NumberReturned", 1 );
      action.setArgumentValue( "TotalMatches", 1 );
    }
    else
    {
      logger.fine("Unsupported BrowseFlag");
      return false;
    }
    
    if( result == null )
      result = "";
    
    //logger.finest("Result: " + result );
    action.setArgumentValue( "Result", result );
    action.setArgumentValue( "UpdateID", "100" );

    return true;
  }

  /**
   * Browse children of a given objectId.
   *
   * @param incomingRequestInterfaceAddr   
   *        Address of network interface for incoming request. This is needed
   *        when functioning in proxy mode to properly support hosts with 
   *        multiple LAN interfaces
   */
  public synchronized String 
  BrowseChildren( String incomingRequestInterfaceAddr,
                  String objId, String filter,
                  int startIndex, int reqCount,
                  String sortCriteria,
                  int[] numMatches,
                  int[] totalMatches )
  {
    logger.fine("Browsing children of obj: " + objId +
                       " filter: " + filter +
                       " startIndex: " + startIndex +
                       " reqCount: " + reqCount +
                       " sortCriteria: " + sortCriteria );

    String result = null;
    CDSObjectList upnpObjList = null;
    
    CDSContainer container = (CDSContainer)objHashTable.get( objId );
    if( container != null )
    {
      //
      // If container is marked as having children, but has no actual 
      // child nodes added yet, check if it has a sole playlist resource.
      // If so, go out and get the playlist (from the internet station
      // on the Web) and convert it to CDS objects
      //
      if( (container.getChildCount() == 99) &&
          (! container.hasChildObjects()) &&
          (container.getResourceCount() == 1) )
      {
        logger.fine("Expanding unresolved playlist");
        
        CDSResource resource = container.getResource(0);
        if( resource.isPlaylist() )
        {
          // Get the playlist items, specifying broadcast flag so returned
          // items are of class CDSAudioBroadcast instead CDSMusicTrack
          upnpObjList = resource.getPlaylistItems( true );

          if( upnpObjList != null )
          {
            for( int n = 0 ; n < upnpObjList.size() ; n++ )
            {
              CDSObject childObj = (CDSObject)upnpObjList.get(n);
              childObj.setId( container.getId() + "/stream_" + (n+1) );
              childObj.setTitle( container.getTitle() + " #" + (n+1) );
              if( childObj.getCreator() == null )
                childObj.setCreator( container.getCreator() );

              childObj.setTitle( container.getTitle() + " #" + (n+1) );
              container.addChild( childObj );
              objHashTable.put( childObj.getId(), childObj );
            }
          }
        }
      }
      else
      {
        upnpObjList = container.getChildList();
      }
    
      //
      // If operating in proxy mode, convert the URL's that point to external
      // sources (net radio most likely) to proxy versions that point to 
      // this server's HTTP proxy server. Only do this for non-playlist
      // resources
      //
      if( device.isProxyModeEnabled() == true )
        upnpObjList = convertObjResourceURLs( upnpObjList,
                                              incomingRequestInterfaceAddr );
      
      result = CDS.toDIDL( upnpObjList, filter,
                           startIndex, reqCount, numMatches,
                           null, null );
    }
    else
    {
      logger.fine("container not found for objectId " + objId );
    }            

    logger.fine( "BrowseChildrenResult: " + result );
    logger.fine( "numMatches = " + numMatches[0] );

    if( upnpObjList == null )
    {
      numMatches[0] = totalMatches[0] = 0;
    }
    else
    {
      totalMatches[0] = upnpObjList.size();
    }
    
    if( result == null )
    {
      logger.fine("Result = NULL numMatches = " + numMatches[0] );
    }
    
    return result;
  }
  
  /**
   * Browse metadata of a given objectId.
   *
   */
  public synchronized String
  BrowseMetadata( String incomingRequestInterfaceAddr,
                  String objId, String filter,
                  int startIndex, int reqCount,
                  String sortCriteria )
  {
    String result = null;

    logger.fine("Browsing metadata of obj: " + objId +
                " filter: " + filter +
                " startIndex: " + startIndex +
                " reqCount: " + reqCount +
                " sortCriteria: " + sortCriteria );

    // Spec says that startIndex should equal 0 for BrowseMetadata
    if( startIndex != 0 )
      logger.warning("Warning - non-zero startIndex in Browse metadata");
      
    CDSObject obj = (CDSObject)objHashTable.get( objId );
    if( obj == null )
    {
      logger.warning("Couldn't find objId '" + objId + "' in hash table!");
    }
    else
    {
      if( device.isProxyModeEnabled() == true )
        obj = convertObjResourceURLs( obj,
                                      incomingRequestInterfaceAddr );

      result = CDS.toDIDL( obj, filter );
    }

    if( result == null )
      logger.fine("BrowseMetadata Result is NULL!" );
    else
      logger.fine( "BrowseMetadataResult: " + result );

    return result;
  }
  
  /**
   *  Convert the URL's in an object to versions that point to 
   *  this server's HTTP proxy server
   *
   *  @return  New object (copy of original) with modified
   *           resource elements
   */ 
  public CDSObject convertObjResourceURLs( CDSObject obj,
                                           String interfaceAddr )
  {
    if( obj.getResourceCount() == 0 )
    {
      // No resources to 'proxify' - just use reference to original object
      return obj;
    }

    CDSObject proxyObj = (CDSObject)obj.clone();
    for( int r = 0 ; r < proxyObj.getResourceCount() ; r++ )
    {
      CDSResource res = proxyObj.getResource(r);
      String url = res.getName();

      //
      // Convert the URL. Example:
      //
      //  http://64.236.34.196:80/stream/2001    
      //
      //    gets changed to:
      // 
      //  http://<proxyIP>:<proxyPort>/64.236.34.196:80/stream/2001
      //
      String proxyURL = URLUtil.urlToProxy( url,
                                            interfaceAddr,
                                            device.getProxyServerPort(),
                                            null );
      res.setName( proxyURL );
    }
    return proxyObj;
  }
  
  /**
   *  Convert the URL's in an object list to versions that point to 
   *  this server's HTTP proxy server. Only change URLs for items, not
   *  folders (which are used to represent playlists)
   */ 
  public CDSObjectList convertObjResourceURLs( CDSObjectList objList,
                                               String interfaceAddr )
  {
    if( objList == null )
      return null;

    CDSObjectList proxyObjList = new CDSObjectList();
    
    for( int n = 0 ; n < objList.size() ; n++ )
    {
      CDSObject obj = objList.getObject(n);

      if( obj instanceof CDSItem )
        proxyObjList.add( convertObjResourceURLs( obj, interfaceAddr ) );
      else
        proxyObjList.add( obj );
    }

    return proxyObjList;
  }
  
  /**
   * Generate a station database web page, 'stationdb.html', and store it
   * in the specified directory. 
   */
  public void generateStationDbHtml( String webFilesDir )
    throws IOException
  {
    logger.fine("Generating stationdb.html from AllStations folder" );

    File dir = new File( webFilesDir );
    if( ! dir.exists() )
    {
      if( ! dir.mkdirs() )
      {
        logger.warning("Error creating Web files dir '" + webFilesDir );
        return;
      }
    }

    String htmlFilename = webFilesDir + "/stationdb.html" ;

    logger.fine("HTML filename is" + htmlFilename );
    FileOutputStream outputStream = new FileOutputStream( htmlFilename );

    generateStationDbHtml( outputStream );
    outputStream.close();
  }

  /**
   * Generate a station database web page based on the contents of the 
   * 'AllStations' folder, and output it to the specified stream.
   */
  public void generateStationDbHtml( OutputStream outputStream )
  {
    PrintWriter writer = new PrintWriter( outputStream );

    RadioServerWebUtil.writeHtmlHeader( writer,
                                        "Radio Server Station Database" );

    // Generate a set of tables, one for each subfolder of the high-level
    // directory
    logger.fine("Processing top-level folder '" +
                allStationsFolder.getTitle() );

    writer.println("<br>");
    writer.println("<h2 align=\"center\">Radio Server Station List</h2>");
    writer.println("<br>");

    // Make separate table for each sub-folder (Genre)

    writer.println("<TABLE border=1 width=90% align=\"center\">");

    writer.println("<TR>");
    writer.println("<TH>Name</TD>");
    writer.println("<TH>Location</TD>");
    writer.println("<TH>Genre(s)</TD>");
    writer.println("<TH>Description</TD>");
    writer.println("</TR>");

    for( int row = 0 ; row < allStationsFolder.getChildCount() ; row++ ) 
    {
      CDSContainer stationObj = (CDSContainer)allStationsFolder.getChild(row);

      logger.fine("Processing station " + stationObj.getTitle() );
      
      if( ! (stationObj instanceof CDSPlaylistContainer) )
      {
        logger.warning("CDSContainer not instance of CDSPlaylistContainer!\n" +
                       "Are there some non-XML and/or subdirectories in\n" +
                       "the AllStations directories");
        continue;
      }

      CDSAudioBroadcast audioBroadcast = 
          (CDSAudioBroadcast)stationObj.getExtraMetadataObj();

      if( audioBroadcast == null )
        logger.warning("audioBroadcast obj null");

      logger.fine("Adding HTML entry for station '" +
                  stationObj.getTitle() +
                  " relation = " + audioBroadcast.getRelation() );
        
      writer.println("<TR>");
      writer.println("<TD width=25%><SMALL><a href=\"" +
                     audioBroadcast.getRelation() + "\">" + 
                     stationObj.getTitle() + "</a></SMALL></TD>");
      writer.println("<TD width=15%><SMALL>" + 
                     audioBroadcast.getRegion() + "</SMALL></TD>");
      writer.println("<TD width=15%><SMALL>" + 
                     audioBroadcast.getGenre() + "</SMALL></TD>");
      writer.println("<TD><SMALL>" + audioBroadcast.getDescription() + 
                     "</SMALL></TD>");
      writer.println("</TR>");
    }

    writer.println("</TABLE>");

    writer.println("</body>");
    writer.println("</html>");
    writer.flush();

    logger.fine("Done generating HTML ");
  }

  /**
   * Generate a station database web page based on the contents of the 
   * 'AllStations' folder, and output it to the specified stream.
   *
   * Each station may have multiple resource (different formats/bitrates)
   * which are all tested in turn
   */
  public void testStations( String resultsFile )
    throws IOException
  {
    logger.fine("Testing stations" );
    FileOutputStream outputStream = new FileOutputStream( resultsFile );
    PrintWriter writer = new PrintWriter( outputStream );

    ShoutcastSnooper shoutcastSnooper = new ShoutcastSnooper();
    
    for( int row = 0 ; row < allStationsFolder.getChildCount() ; row++ ) 
    {
      CDSContainer stationObj = (CDSContainer)allStationsFolder.getChild(row);

      if( ! (stationObj instanceof CDSPlaylistContainer) )
      {
        logger.warning("CDSContainer not instance of CDSPlaylistContainer!\n" +
                       "Are there some non-XML and/or subdirectories in\n" +
                       "the AllStations directories");
        continue;
      }

      writer.println("-------------------------------------------------------------------------");
      writer.println("Processing radio station '" + stationObj.getTitle() +
                     "'" );
      System.out.println("Processing station " + stationObj.getTitle() );
      

      for( int n = 0 ; n < stationObj.getChildCount(); n++ ) 
      {
        CDSContainer stationFmt = (CDSContainer)stationObj.getChild(n);
        writer.println("Testing fmt/bitrate" + stationFmt.getTitle() );
        System.out.println("Testing fmt/bitrate" + stationFmt.getTitle() );

        int resourceCount = stationFmt.getResourceCount();
        CDSResource resource =  stationFmt.getResource(0);
        writer.println("  Protocol: " + resource.getProtocolInfo() +
                       "\n  Resource: " + resource.getName() );

        shoutcastSnooper.readPlaylist( resource, writer );
      }
    }

    writer.flush();
    outputStream.close();
  }

}


